package com.sky.templatehttpd;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Properties;
import java.util.TimeZone;

/**
 * HTTP response content.
 */
public class Response {
    /** Response content text charset. */
    private static final String CHARSET = "UTF-8";
    /** Line ending string. */
    private static final String ENDLINE = "\r\n";
    /** Buffer size to copy data stream. */
    private static final int BUFFER_SIZE = 2048;
    /** GMT date formatter. */
    private static final SimpleDateFormat GMTFRMT;

    static {
        GMTFRMT = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
        GMTFRMT.setTimeZone(TimeZone.getTimeZone("GMT"));
    }
    /** Common HTTP response status codes. */
    public static final String HTTP_OK = "200 OK";
    public static final String HTTP_REDIRECT = "301 Moved Permanently";
    public static final String HTTP_FORBIDDEN = "403 Forbidden";
    public static final String HTTP_NOTFOUND = "404 Not Found";
    public static final String HTTP_BADREQUEST = "400 Bad Request";
    public static final String HTTP_INTERNALERROR = "500 Internal Server Error";
    public static final String HTTP_NOTIMPLEMENTED = "501 Not Implemented";
    /** Common mime types for dynamic content. */
    public static final String MIME_PLAINTEXT = "text/plain; charset="+CHARSET;
    public static final String MIME_HTML = "text/html; charset="+CHARSET;
    public static final String MIME_JS = "application/x-javascript; charset="+CHARSET;
    public static final String MIME_DEFAULT_BINARY = "application/octet-stream";
    /** HTTP status code after processing, e.g. "200 OK", HTTP_OK. */
    private final String status;
    /** MIME type of content, e.g. "text/html". */
    private final String mimeType;
    /** Data of the response, may be null. */
    private final InputStream data;
    /** Headers for the HTTP response. Use addHeader() to add lines. */
    private final Properties header;

    /**
     * Default constructor: response = HTTP_OK, data = mime = 'null'.
     */
    public Response() {
        this(HTTP_OK, null, (String) null);
    }

    /**
     * Convenience method that makes an InputStream out of given text.
     * @param status HTTP status code after processing, e.g. "200 OK", HTTP_OK.
     * @param mimeType MIME type of content, e.g. "text/html".
     * @param txt Data text.
     */
    public Response(String status, String mimeType, String txt) {
        this(status, mimeType, txt != null ? new ByteArrayInputStream(txt.getBytes()) : null);
    }

    /**
     * Create a new response with empty header.
     * Fill header using method addHeader.
     * @param status HTTP status code after processing, e.g. "200 OK", HTTP_OK.
     * @param mimeType MIME type of content, e.g. "text/html".
     * @param data Data of the response, may be null.
     */
    public Response(String status, String mimeType, InputStream data) {
        this.status = status;
        this.mimeType = mimeType;
        this.data = data;
        this.header = new Properties();
    }

    /**
     * Fully filled response constructor.
     * @param status HTTP status code after processing, e.g. "200 OK", HTTP_OK.
     * @param mimeType MIME type of content, e.g. "text/html".
     * @param header Headers for the HTTP response.
     * @param data Data of the response, may be null.
     */
    public Response(String status, String mimeType, Properties header, InputStream data) {
        this.status = status;
        this.mimeType = mimeType;
        this.data = data;
        this.header = header;
    }

    /**
     * Adds given line to the header.
     * @param name Name of the header parameter.
     * @param value Value of the header parameter.
     */
    public void addHeader(String name, String value) {
        header.put(name, value);
    }

    /**
     * Sends response to a stream, and close it on succeed send.
     * throws NullPointerException on null stream or status.
     * @param stream Stream to write on.
     * @throws IOException Throw by stream on error.
     */
    public void send(OutputStream stream) throws IOException {
        if (stream == null) {
            throw new NullPointerException("Response stream can't be null.");
        }

        if (status == null) {
            throw new NullPointerException("Response status can't be null.");
        }

        writeHeader(stream);
        writeData(stream);

        stream.flush();
        stream.close();
    }

    /**
     * Write Response HTTP header to given stream.
     * @param stream Stream to write on.
     */
    private void writeHeader(OutputStream stream) {

        PrintWriter writer = new PrintWriter(stream);

        writer.print("HTTP/1.0 " + status + ENDLINE);
        if (mimeType != null) {
            writer.print("Content-Type: " + mimeType + ENDLINE);
        }
        if (header == null || header.getProperty("Date") == null) {
            writer.print("Date: " + GMTFRMT.format(new Date()) + ENDLINE);
        }
        if (header != null) {
            Enumeration<?> propertieNames = header.propertyNames();
            while (propertieNames.hasMoreElements()) {
                String propertyName = (String) propertieNames.nextElement();
                String value = header.getProperty(propertyName);
                writer.print(propertyName + ": " + value + ENDLINE);
            }
        }
        writer.print(ENDLINE);
        writer.flush();
    }

    /**
     * Write Response HTTP data to given stream.
     * @param stream Stream to write on.
     * @throws IOException When inner stream throws.
     */
    private void writeData(OutputStream stream) throws IOException {
        if (data != null) {
            byte[] buff = new byte[BUFFER_SIZE];
            while (true) {
                int read = data.read(buff, 0, BUFFER_SIZE);
                if (read <= 0) {
                    break;
                }
                stream.write(buff, 0, read);
            }

            data.close();
        }
    }
}
